package com.hqd.ch03.v50.web.context.support;

import com.hqd.ch03.utils.ClassUtils;
import com.hqd.ch03.v50.beans.factory.config.ConfigurableListableBeanFactory;
import com.hqd.ch03.v50.core.env.MutablePropertySources;
import com.hqd.ch03.v50.core.env.PropertySource;
import com.hqd.ch03.v50.factory.ObjectFactory;
import com.hqd.ch03.v50.web.context.ConfigurableWebApplicationContext;
import com.hqd.ch03.v50.web.context.WebApplicationContext;
import com.hqd.ch03.v50.web.context.request.*;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpSession;
import java.io.Serializable;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

public abstract class WebApplicationContextUtils {

    private static final boolean jsfPresent =
            ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());

    public static WebApplicationContext getRequiredWebApplicationContext(ServletContext sc) throws IllegalStateException {
        WebApplicationContext wac = getWebApplicationContext(sc);
        if (wac == null) {
            throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?");
        }
        return wac;
    }


    public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
        return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    }

    /**
     * Find a custom {@code WebApplicationContext} for this web app.
     *
     * @param sc       the ServletContext to find the web application context for
     * @param attrName the name of the ServletContext attribute to look for
     * @return the desired WebApplicationContext for this web app, or {@code null} if none
     */

    public static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) {
        Object attr = sc.getAttribute(attrName);
        if (attr == null) {
            return null;
        }
        if (attr instanceof RuntimeException) {
            throw (RuntimeException) attr;
        }
        if (attr instanceof Error) {
            throw (Error) attr;
        }
        if (attr instanceof Exception) {
            throw new IllegalStateException((Exception) attr);
        }
        if (!(attr instanceof WebApplicationContext)) {
            throw new IllegalStateException("Context attribute is not of type WebApplicationContext: " + attr);
        }
        return (WebApplicationContext) attr;
    }

    /**
     * Find a unique {@code WebApplicationContext} for this web app: either the
     * root web app context (preferred) or a unique {@code WebApplicationContext}
     * among the registered {@code ServletContext} attributes (typically coming
     * from a single {@code DispatcherServlet} in the current web application).
     * <p>Note that {@code DispatcherServlet}'s exposure of its context can be
     * controlled through its {@code publishContext} property, which is {@code true}
     * by default but can be selectively switched to only publish a single context
     * despite multiple {@code DispatcherServlet} registrations in the web app.
     *
     * @param sc the ServletContext to find the web application context for
     * @return the desired WebApplicationContext for this web app, or {@code null} if none
     * @see #getWebApplicationContext(ServletContext)
     * @see ServletContext#getAttributeNames()
     * @since 4.2
     */

    public static WebApplicationContext findWebApplicationContext(ServletContext sc) {
        WebApplicationContext wac = getWebApplicationContext(sc);
        if (wac == null) {
            Enumeration<String> attrNames = sc.getAttributeNames();
            while (attrNames.hasMoreElements()) {
                String attrName = attrNames.nextElement();
                Object attrValue = sc.getAttribute(attrName);
                if (attrValue instanceof WebApplicationContext) {
                    if (wac != null) {
                        throw new IllegalStateException("No unique WebApplicationContext found: more than one " +
                                "DispatcherServlet registered with publishContext=true?");
                    }
                    wac = (WebApplicationContext) attrValue;
                }
            }
        }
        return wac;
    }


    /**
     * Register web-specific scopes ("request", "session", "globalSession")
     * with the given BeanFactory, as used by the WebApplicationContext.
     *
     * @param beanFactory the BeanFactory to configure
     */
    public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory) {
        registerWebApplicationScopes(beanFactory, null);
    }

    /**
     * Register web-specific scopes ("request", "session", "globalSession", "application")
     * with the given BeanFactory, as used by the WebApplicationContext.
     *
     * @param beanFactory the BeanFactory to configure
     * @param sc          the ServletContext that we're running within
     */
    public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory,
                                                    ServletContext sc) {

        beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope());
        beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope());
        if (sc != null) {
            ServletContextScope appScope = new ServletContextScope(sc);
            beanFactory.registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
            // Register as ServletContext attribute, for ContextCleanupListener to detect it.
            sc.setAttribute(ServletContextScope.class.getName(), appScope);
        }

        beanFactory.registerResolvableDependency(ServletRequest.class, new RequestObjectFactory());
        beanFactory.registerResolvableDependency(ServletResponse.class, new ResponseObjectFactory());
        beanFactory.registerResolvableDependency(HttpSession.class, new SessionObjectFactory());
        beanFactory.registerResolvableDependency(WebRequest.class, new WebRequestObjectFactory());
        if (jsfPresent) {
            FacesDependencyRegistrar.registerFacesDependencies(beanFactory);
        }
    }

    /**
     * Register web-specific environment beans ("contextParameters", "contextAttributes")
     * with the given BeanFactory, as used by the WebApplicationContext.
     *
     * @param bf the BeanFactory to configure
     * @param sc the ServletContext that we're running within
     */
    public static void registerEnvironmentBeans(ConfigurableListableBeanFactory bf, ServletContext sc) {
        registerEnvironmentBeans(bf, sc, null);
    }

    /**
     * Register web-specific environment beans ("contextParameters", "contextAttributes")
     * with the given BeanFactory, as used by the WebApplicationContext.
     *
     * @param bf             the BeanFactory to configure
     * @param servletContext the ServletContext that we're running within
     * @param servletConfig  the ServletConfig
     */
    public static void registerEnvironmentBeans(ConfigurableListableBeanFactory bf,
                                                ServletContext servletContext, ServletConfig servletConfig) {

        if (servletContext != null && !bf.containsBean(WebApplicationContext.SERVLET_CONTEXT_BEAN_NAME)) {
            bf.registerSingleton(WebApplicationContext.SERVLET_CONTEXT_BEAN_NAME, servletContext);
        }

        if (servletConfig != null && !bf.containsBean(ConfigurableWebApplicationContext.SERVLET_CONFIG_BEAN_NAME)) {
            bf.registerSingleton(ConfigurableWebApplicationContext.SERVLET_CONFIG_BEAN_NAME, servletConfig);
        }

        if (!bf.containsBean(WebApplicationContext.CONTEXT_PARAMETERS_BEAN_NAME)) {
            Map<String, String> parameterMap = new HashMap<>();
            if (servletContext != null) {
                Enumeration<?> paramNameEnum = servletContext.getInitParameterNames();
                while (paramNameEnum.hasMoreElements()) {
                    String paramName = (String) paramNameEnum.nextElement();
                    parameterMap.put(paramName, servletContext.getInitParameter(paramName));
                }
            }
            if (servletConfig != null) {
                Enumeration<?> paramNameEnum = servletConfig.getInitParameterNames();
                while (paramNameEnum.hasMoreElements()) {
                    String paramName = (String) paramNameEnum.nextElement();
                    parameterMap.put(paramName, servletConfig.getInitParameter(paramName));
                }
            }
            bf.registerSingleton(WebApplicationContext.CONTEXT_PARAMETERS_BEAN_NAME,
                    Collections.unmodifiableMap(parameterMap));
        }

        if (!bf.containsBean(WebApplicationContext.CONTEXT_ATTRIBUTES_BEAN_NAME)) {
            Map<String, Object> attributeMap = new HashMap<>();
            if (servletContext != null) {
                Enumeration<?> attrNameEnum = servletContext.getAttributeNames();
                while (attrNameEnum.hasMoreElements()) {
                    String attrName = (String) attrNameEnum.nextElement();
                    attributeMap.put(attrName, servletContext.getAttribute(attrName));
                }
            }
            bf.registerSingleton(WebApplicationContext.CONTEXT_ATTRIBUTES_BEAN_NAME,
                    Collections.unmodifiableMap(attributeMap));
        }
    }

    /**
     * Convenient variant of {@link #initServletPropertySources(MutablePropertySources,
     * ServletContext, ServletConfig)} that always provides {@code null} for the
     * {@link ServletConfig} parameter.
     *
     * @see #initServletPropertySources(MutablePropertySources, ServletContext, ServletConfig)
     */
    public static void initServletPropertySources(MutablePropertySources propertySources, ServletContext servletContext) {
        initServletPropertySources(propertySources, servletContext, null);
    }

    /**
     * Replace {@code Servlet}-based {@link StubPropertySource stub property sources} with
     * actual instances populated with the given {@code servletContext} and
     * {@code servletConfig} objects.
     * <p>This method is idempotent with respect to the fact it may be called any number
     * of times but will perform replacement of stub property sources with their
     * corresponding actual property sources once and only once.
     *
     * @param sources        the {@link MutablePropertySources} to initialize (must not
     *                       be {@code null})
     * @param servletContext the current {@link ServletContext} (ignored if {@code null}
     *                       or if the {@link StandardServletEnvironment#SERVLET_CONTEXT_PROPERTY_SOURCE_NAME
     *                       servlet context property source} has already been initialized)
     * @param servletConfig  the current {@link ServletConfig} (ignored if {@code null}
     *                       or if the {@link StandardServletEnvironment#SERVLET_CONFIG_PROPERTY_SOURCE_NAME
     *                       servlet config property source} has already been initialized)
     * @see org.springframework.core.env.PropertySource.StubPropertySource
     * @see org.springframework.core.env.ConfigurableEnvironment#getPropertySources()
     */
    public static void initServletPropertySources(MutablePropertySources sources,
                                                  ServletContext servletContext, ServletConfig servletConfig) {

        String name = StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME;
        if (servletContext != null && sources.get(name) instanceof PropertySource.StubPropertySource) {
            sources.replace(name, new ServletContextPropertySource(name, servletContext));
        }
        name = StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME;
        if (servletConfig != null && sources.get(name) instanceof PropertySource.StubPropertySource) {
            sources.replace(name, new ServletConfigPropertySource(name, servletConfig));
        }
    }

    /**
     * Return the current RequestAttributes instance as ServletRequestAttributes.
     *
     * @see RequestContextHolder#currentRequestAttributes()
     */
    private static ServletRequestAttributes currentRequestAttributes() {
        RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();
        if (!(requestAttr instanceof ServletRequestAttributes)) {
            throw new IllegalStateException("Current request is not a servlet request");
        }
        return (ServletRequestAttributes) requestAttr;
    }


    /**
     * Factory that exposes the current request object on demand.
     */
    @SuppressWarnings("serial")
    private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {

        @Override
        public ServletRequest getObject() {
            return currentRequestAttributes().getRequest();
        }

        @Override
        public String toString() {
            return "Current HttpServletRequest";
        }
    }


    /**
     * Factory that exposes the current response object on demand.
     */
    @SuppressWarnings("serial")
    private static class ResponseObjectFactory implements ObjectFactory<ServletResponse>, Serializable {

        @Override
        public ServletResponse getObject() {
            ServletResponse response = currentRequestAttributes().getResponse();
            if (response == null) {
                throw new IllegalStateException("Current servlet response not available - " +
                        "consider using RequestContextFilter instead of RequestContextListener");
            }
            return response;
        }

        @Override
        public String toString() {
            return "Current HttpServletResponse";
        }
    }


    /**
     * Factory that exposes the current session object on demand.
     */
    @SuppressWarnings("serial")
    private static class SessionObjectFactory implements ObjectFactory<HttpSession>, Serializable {

        @Override
        public HttpSession getObject() {
            return currentRequestAttributes().getRequest().getSession();
        }

        @Override
        public String toString() {
            return "Current HttpSession";
        }
    }


    /**
     * Factory that exposes the current WebRequest object on demand.
     */
    @SuppressWarnings("serial")
    private static class WebRequestObjectFactory implements ObjectFactory<WebRequest>, Serializable {

        @Override
        public WebRequest getObject() {
            ServletRequestAttributes requestAttr = currentRequestAttributes();
            return new ServletWebRequest(requestAttr.getRequest(), requestAttr.getResponse());
        }

        @Override
        public String toString() {
            return "Current ServletWebRequest";
        }
    }


    /**
     * Inner class to avoid hard-coded JSF dependency.
     */
    private static class FacesDependencyRegistrar {

        public static void registerFacesDependencies(ConfigurableListableBeanFactory beanFactory) {
        }
    }

}
